<?php

/**
 * Abstract class to emulate a PDOStatement for native database adapters.
 *
 * @category   Zend
 * @package    Db
 * @subpackage Statement
 * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
abstract class Database_Statement 
{

	/**
	 * @var resource|object The driver level statement object/resource
	 */
	protected $_stmt = null;

	/**
	 * @var Database_Adapter
	 */
	protected $_adapter = null;

	/**
	 * The current fetch mode.
	 *
	 * @var integer
	 */
	protected $_fetchMode = Database::FETCH_ASSOC;

	/**
	 * Attributes.
	 *
	 * @var array
	 */
	protected $_attribute = array();

	/**
	 * Column result bindings.
	 *
	 * @var array
	 */
	protected $_bindColumn = array();

	/**
	 * Query parameter bindings; covers bindParam() and bindValue().
	 *
	 * @var array
	 */
	protected $_bindParam = array();

	/**
	 * SQL string split into an array at placeholders.
	 *
	 * @var array
	 */
	protected $_sqlSplit = array();

	/**
	 * Parameter placeholders in the SQL string by position in the split array.
	 *
	 * @var array
	 */
	protected $_sqlParam = array();

	/**
	 * @var Database_Profiler_Query
	 */
	protected $_queryId = null;

	/**
	 * Constructor for a statement.
	 *
	 * @param Database_Adapter $adapter
	 * @param mixed $sql Either a string or Database_Select.
	 */
	public function __construct($adapter, $sql)
	{
		$this -> _adapter = $adapter;
		if ($sql instanceof Database_Select)
		{
			$sql = $sql -> assemble();
		}
		$this -> _parseParameters($sql);
	 $this -> _prepare($sql);
	}

	/**
	 * Internal method called by abstract statment constructor to setup
	 * the driver level statement
	 *
	 * @return void
	 */
	protected function _prepare($sql)
	{
		return;
	}

	/**
	 * @param string $sql
	 * @return void
	 */
	protected function _parseParameters($sql)
	{
		$sql = $this -> _stripQuoted($sql);

		// split into text and params
		$this -> _sqlSplit = preg_split('/(\?|\:[a-zA-Z0-9_]+)/', $sql, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);

		// map params
		$this -> _sqlParam = array();
		foreach ($this->_sqlSplit as $key => $val)
		{
			if ($val == '?')
			{
				if ($this -> _adapter -> supportsParameters('positional') === false)
				{
					/**
					 * @see Exception
					 */

					throw new Exception("Invalid bind-variable position '$val'");
				}
			}
			else
			if ($val[0] == ':')
			{
				if ($this -> _adapter -> supportsParameters('named') === false)
				{
					throw new Exception("Invalid bind-variable name '$val'");
				}
			}
			$this -> _sqlParam[] = $val;
		}

		// set up for binding
		$this -> _bindParam = array();
	}

	/**
	 * Remove parts of a SQL string that contain quoted strings
	 * of values or identifiers.
	 *
	 * @param string $sql
	 * @return string
	 */
	protected function _stripQuoted($sql)
	{
		// get the character for delimited id quotes,
		// this is usually " but in MySQL is `
		$d = $this -> _adapter -> quoteIdentifier('a');
		$d = $d[0];

		// get the value used as an escaped delimited id quote,
		// e.g. \" or "" or \`
		$de = $this -> _adapter -> quoteIdentifier($d);
		$de = substr($de, 1, 2);
		$de = str_replace('\\', '\\\\', $de);

		// get the character for value quoting
		// this should be '
		$q = $this -> _adapter -> quote('a');
		$q = $q[0];

		// get the value used as an escaped quote,
		// e.g. \' or ''
		$qe = $this -> _adapter -> quote($q);
		$qe = substr($qe, 1, 2);
		$qe = str_replace('\\', '\\\\', $qe);

		// get a version of the SQL statement with all quoted
		// values and delimited identifiers stripped out
		// remove "foo\"bar"
		$sql = preg_replace("/$q($qe|\\\\{2}|[^$q])*$q/", '', $sql);
		// remove 'foo\'bar'
		if (!empty($q))
		{
			$sql = preg_replace("/$q($qe|[^$q])*$q/", '', $sql);
		}

		return $sql;
	}

	/**
	 * Bind a column of the statement result set to a PHP variable.
	 *
	 * @param string $column Name the column in the result set, either by
	 *                       position or by name.
	 * @param mixed  $param  Reference to the PHP variable containing the value.
	 * @param mixed  $type   OPTIONAL
	 * @return bool
	 */
	public function bindColumn($column, &$param, $type = null)
	{
		$this -> _bindColumn[$column] = &$param;
		return true;
	}

	/**
	 * Binds a parameter to the specified variable name.
	 *
	 * @param mixed $parameter Name the parameter, either integer or string.
	 * @param mixed $variable  Reference to PHP variable containing the value.
	 * @param mixed $type      OPTIONAL Datatype of SQL parameter.
	 * @param mixed $length    OPTIONAL Length of SQL parameter.
	 * @param mixed $options   OPTIONAL Other options.
	 * @return bool
	 */
	public function bindParam($parameter, &$variable, $type = null, $length = null, $options = null)
	{
		if (!is_int($parameter) && !is_string($parameter))
		{
			/**
			 * @see Exception
			 */

			throw new Exception('Invalid bind-variable position');
		}

		$position = null;
		if (($intval = (int)$parameter) > 0 && $this -> _adapter -> supportsParameters('positional'))
		{
			if ($intval >= 1 || $intval <= count($this -> _sqlParam))
			{
				$position = $intval;
			}
		}
		else
		if ($this -> _adapter -> supportsParameters('named'))
		{
			if ($parameter[0] != ':')
			{
				$parameter = ':' . $parameter;
			}
			if (in_array($parameter, $this -> _sqlParam) !== false)
			{
				$position = $parameter;
			}
		}

		if ($position === null)
		{
			/**
			 * @see Exception
			 */
			throw new Exception("Invalid bind-variable position '$parameter'");
		}

		// Finally we are assured that $position is valid
		$this -> _bindParam[$position] = &$variable;
		return $this -> _bindParam($position, $variable, $type, $length, $options);
	}

	/**
	 * Binds a value to a parameter.
	 *
	 * @param mixed $parameter Name the parameter, either integer or string.
	 * @param mixed $value     Scalar value to bind to the parameter.
	 * @param mixed $type      OPTIONAL Datatype of the parameter.
	 * @return bool
	 */
	public function bindValue($parameter, $value, $type = null)
	{
		return $this -> bindParam($parameter, $value, $type);
	}

	/**
	 * Executes a prepared statement.
	 *
	 * @param array $params OPTIONAL Values to bind to parameter placeholders.
	 * @return bool
	 */
	public function execute(array $params = null)
	{
		/*
		 * Simple case - no query profiler to manage.
		 */
		if (PROFILLING)
		{
			$token = Nuo_Profiler::start('sql',$this -> _stmt -> queryString);
		}

		$retval = $this -> _execute($params);

		if (PROFILLING)
		{
			Nuo_Profiler::stop($token);
		}

		return $retval;
	}

	/**
	 * Returns an array containing all of the result set rows.
	 *
	 * @param int $style OPTIONAL Fetch mode.
	 * @param int $col   OPTIONAL Column number, if fetch mode is by column.
	 * @return array Collection of rows, each in a format by the fetch mode.
	 */
	public function fetchAll($style = null, $col = null)
	{
		$data = array();
		if ($style === Database::FETCH_COLUMN && $col === null)
		{
			$col = 0;
		}
		if ($col === null)
		{
			while ($row = $this -> fetch($style))
			{
				$data[] = $row;
			}
		}
		else
		{
			while (false !== ($val = $this -> fetchColumn($col)))
			{
				$data[] = $val;
			}
		}
		return $data;
	}

	/**
	 * Returns a single column from the next row of a result set.
	 *
	 * @param int $col OPTIONAL Position of the column to fetch.
	 * @return string One value from the next row of result set, or false.
	 */
	public function fetchColumn($col = 0)
	{
		$data = array();
		$col = (int)$col;
		$row = $this -> fetch(Database::FETCH_NUM);
		if (!is_array($row))
		{
			return false;
		}
		return $row[$col];
	}

	/**
	 * Fetches the next row and returns it as an object.
	 *
	 * @param string $class  OPTIONAL Name of the class to create.
	 * @param array  $config OPTIONAL Constructor arguments for the class.
	 * @return mixed One object instance of the specified class, or false.
	 */
	public function fetchObject($class = 'stdClass', array $config = array())
	{
		$obj = new $class($config);
		$row = $this -> fetch(Database::FETCH_ASSOC);
		if (!is_array($row))
		{
			return false;
		}
		foreach ($row as $key => $val)
		{
			$obj -> $key = $val;
		}
		return $obj;
	}

	/**
	 * Retrieve a statement attribute.
	 *
	 * @param string $key Attribute name.
	 * @return mixed      Attribute value.
	 */
	public function getAttribute($key)
	{
		if (array_key_exists($key, $this -> _attribute))
		{
			return $this -> _attribute[$key];
		}
	}

	/**
	 * Set a statement attribute.
	 *
	 * @param string $key Attribute name.
	 * @param mixed  $val Attribute value.
	 * @return bool
	 */
	public function setAttribute($key, $val)
	{
		$this -> _attribute[$key] = $val;
	}

	/**
	 * Set the default fetch mode for this statement.
	 *
	 * @param int   $mode The fetch mode.
	 * @return bool
	 * @throws Exception
	 */
	public function setFetchMode($mode)
	{
		switch ($mode)
		{
			case Database::FETCH_NUM :
			case Database::FETCH_ASSOC :
			case Database::FETCH_BOTH :
			case Database::FETCH_OBJ :
				$this -> _fetchMode = $mode;
				break;
			case Database::FETCH_BOUND :
			default :
				$this -> closeCursor();
				/**
				 * @see Exception
				 */

				throw new Exception('invalid fetch mode');
				break;
		}
	}

	/**
	 * Helper function to map retrieved row
	 * to bound column variables
	 *
	 * @param array $row
	 * @return bool True
	 */
	public function _fetchBound($row)
	{
		foreach ($row as $key => $value)
		{
			// bindColumn() takes 1-based integer positions
			// but fetch() returns 0-based integer indexes
			if (is_int($key))
			{
				$key++;
			}
			// set results only to variables that were bound previously
			if (isset($this -> _bindColumn[$key]))
			{
				$this -> _bindColumn[$key] = $value;
			}
		}
		return true;
	}

	/**
	 * Gets the Database_Adapter for this
	 * particular Database_Statement object.
	 *
	 * @return Database_Adapter
	 */
	public function getAdapter()
	{
		return $this -> _adapter;
	}

	/**
	 * Gets the resource or object setup by the
	 * _parse
	 * @return unknown_type
	 */
	public function getDriverStatement()
	{
		return $this -> _stmt;
	}

}
